17
04月
图形界面 XI: 表格单元中的文本编辑框和组合框 (统合构建15)
内容
- 概述
- 调整窗口大小
- 表格单元中的文本框和组合框
- 应用测试
- 结束语
概述
首篇文章 图形界面 I: 函数库结构的准备 (第 1 章) 详细研究了这个函数库。每篇文章的末尾, 附加了当前开发阶段的完整版函数库。文件必须放置于存档中所在的相同目录下。
下一次更新侧重于表格控件 (CTable 类)。前一版中 可以将复选框和按钮添加到表格单元中。我们用文本框和组合框扩展这些控件的阵容。新版中还增加了在应用程序运行时管理窗口大小的功能。
调整窗口大小
为了方便使用列表视图、表格或多行文本框, 通常需要减少窗口或将其最大化到整个图表。有多种方式来管理窗口大小。
- 通过单击特殊按钮即可从正常到全屏快速切换模式。
- 双击窗口标题也能将窗口最大化为全屏。再次双击将窗口返回到前一次状态。
- 可以通过使用鼠标左键拖动其边框来调整窗口大小。
我们来观察这是如何在函数库中实现的。
为了创建全屏模式按钮, 单独声明了 CButton 类的实例。公有 CButton::GetFullscreenButtonPointer() 方法设计用于获得指向该按钮的指针。省缺情况下, 窗体上的按钮被禁用。使用 CButton::FullscreenButtonIsUsed() 方法启用该按钮。
//+------------------------------------------------------------------+ //| 控件的窗体类 | //+------------------------------------------------------------------+ class CWindow : public CElement { private: //--- 创建窗体的对象 CButton m_button_fullscreen; //--- 存在将窗口最大化为全屏模式的按钮 bool m_fullscreen_button; //--- public: //--- 返回指向窗体按钮的指针 CButton *GetFullscreenButtonPointer(void) { return(::GetPointer(m_button_fullscreen)); } //--- 使用全屏按钮 void FullscreenButtonIsUsed(const bool state) { m_fullscreen_button=state; } bool FullscreenButtonIsUsed(void) const { return(m_fullscreen_button); } };
全屏按钮是由通用方法 CWindow::CreateButtons() 方法 (见下面的代码清单) 创建的, 其中创建了所有启用的按钮。类似于显示工具提示和最小化窗体的按钮, 全屏模式按钮只能在主窗口中使用。
//+------------------------------------------------------------------+ //| 在窗体上创建按钮 | //+------------------------------------------------------------------+ #resource "\\Images\\EasyAndFastGUI\\Controls\\full_screen.bmp" #resource "\\Images\\EasyAndFastGUI\\Controls\\minimize_to_window.bmp" //--- bool CWindow::CreateButtons(void) { //--- 如果程序类型是脚本, 离开 if(CElementBase::ProgramType()==PROGRAM_SCRIPT) return(true); //--- 计数器, 大小, 数量 int i=0,x_size=20; int buttons_total=4; //--- 文件的路径 string icon_file=""; //--- 捕获区域中的异常 m_right_limit=0; //--- CButton *button_obj=NULL; //--- for(int b=0; b<buttons_total; b++) { ... else if(b==1) { m_button_fullscreen.MainPointer(this); //--- 如果 (1) 按钮未启用或 (2) 它是一个对话框, 离开 if(!m_fullscreen_button || m_window_type==W_DIALOG) continue; //--- button_obj=::GetPointer(m_button_fullscreen); icon_file="Images\\EasyAndFastGUI\\Controls\\full_screen.bmp"; } ... } //--- return(true); }
窗口的最小尺寸自动设置为创建控件时指定的大小。但这些值可以被覆盖。在当前版本中, 不能将窗口大小设置为小于 200 x 200 像素。这是在初始化控件属性的方法中进行控制的 — CWindow::InitializeProperties()。
class CWindow : public CElement { private: //--- 最小窗口大小 int m_minimum_x_size; int m_minimum_y_size; //--- public: //--- 设置最小窗口大小 void MinimumXSize(const int x_size) { m_minimum_x_size=x_size; } void MinimumYSize(const int y_size) { m_minimum_y_size=y_size; } }; //+------------------------------------------------------------------+ //| 构造函数 | //+------------------------------------------------------------------+ CWindow::CWindow(void) : m_minimum_x_size(0), m_minimum_y_size(0) { ... } //+------------------------------------------------------------------+ //| 初始化属性 | //+------------------------------------------------------------------+ void CWindow::InitializeProperties(const long chart_id,const int subwin,const string caption_text,const int x_gap,const int y_gap) { ... m_x_size =(m_x_size<1)? 200 : m_x_size; m_y_size =(m_y_size<1)? 200 : m_y_size; ... m_minimum_x_size =(m_minimum_x_size<200)? m_x_size : m_minimum_x_size; m_minimum_y_size =(m_minimum_y_size<200)? m_y_size : m_minimum_y_size; ... }
在调整窗口大小最大化到全屏之前, 必须保存当前尺寸、坐标和自动大小调整模式 (如果已设置)。这些值存储在该类的特殊私有字段中:
class CWindow : public CElement { private: //--- 切换到全屏前窗口的最后坐标和尺寸 int m_last_x; int m_last_y; int m_last_x_size; int m_last_y_size; bool m_last_auto_xresize; bool m_last_auto_yresize; };
CWindow::OnClickFullScreenButton() 方法用于处理全屏按钮上的点击。它首先检查控件的标识符和索引, 然后将代码分为两个模块:
- 如果窗口当前未最大化, 请将其切换到全屏模式。接下来, 获得当前图表维度, 当前尺寸, 窗口坐标和自动大小调整模式存储在类的特殊字段中。由于全屏模式需要在主图的尺寸发生变化时自动调整窗体, 所以需要 启用自动大小调整的模式。之后, 设置窗口大小。同时, 窗口位置设置在左上角, 以便填充整个图表空间。按钮中的图标被替换为另一个。
- 如果窗口当前最大化, 请将其切换到以前的窗口大小。在此, 窗口的自动更改模式将切换到先前的状态。然后, 取决于哪种模式被禁用, 设置先前的窗口大小。此外, 设置按钮的上一次位置和相应的图标。
在 CWindow::OnClickFullScreenButton() 方法的末尾, 窗体重绘:
class CWindow : public CElement { private: //--- 窗口状态为全屏模式 bool m_is_fullscreen; //--- public: //--- 切换到全屏或以前的窗口大小 bool OnClickFullScreenButton(const int id=WRONG_VALUE,const int index=WRONG_VALUE); }; //+------------------------------------------------------------------+ //| 切换到全屏或以前的窗口大小 | //+------------------------------------------------------------------+ bool CWindow::OnClickFullScreenButton(const int id=WRONG_VALUE,const int index=WRONG_VALUE) { //--- 如果有外部调用, 择检查控件的标识符和索引 int check_id =(id!=WRONG_VALUE)? id : CElementBase::Id(); int check_index =(index!=WRONG_VALUE)? index : CElementBase::Index(); //--- 如果索引不匹配, 离开 if(check_id!=m_button_fullscreen.Id() || check_index!=m_button_fullscreen.Index()) return(false); //--- 如果窗口非全屏 if(!m_is_fullscreen) { //--- 切换到全屏 m_is_fullscreen=true; //--- 获取图表窗口的当前维度 SetWindowProperties(); //--- 存储窗体的当前坐标和尺寸 m_last_x =m_x; m_last_y =m_y; m_last_x_size =m_x_size; m_last_y_size =m_full_height; m_last_auto_xresize =m_auto_xresize_mode; m_last_auto_yresize =m_auto_yresize_mode; //--- 启用窗体的自动大小调整 m_auto_xresize_mode=true; m_auto_yresize_mode=true; //--- 窗体最大化到整个图表 ChangeWindowWidth(m_chart.WidthInPixels()-2); ChangeWindowHeight(m_chart.HeightInPixels(m_subwin)-3); //--- 更新位置 m_x=m_y=1; Moving(m_x,m_y); //--- 更换按钮图标 m_button_fullscreen.IconFile("Images\\EasyAndFastGUI\\Controls\\minimize_to_window.bmp"); m_button_fullscreen.IconFileLocked("Images\\EasyAndFastGUI\\Controls\\minimize_to_window.bmp"); } //--- 如果窗口处于全屏 else { //--- 切换到上一次窗口大小 m_is_fullscreen=false; //--- 禁用自动大小调整 m_auto_xresize_mode=m_last_auto_xresize; m_auto_yresize_mode=m_last_auto_yresize; //--- 如果禁用该模式, 设置为前次的大小 if(!m_auto_xresize_mode) ChangeWindowWidth(m_last_x_size); if(!m_auto_yresize_mode) ChangeWindowHeight(m_last_y_size); //--- 更新位置 m_x=m_last_x; m_y=m_last_y; Moving(m_x,m_y); //--- 更换按钮图标 m_button_fullscreen.IconFile("Images\\EasyAndFastGUI\\Controls\\full_screen.bmp"); m_button_fullscreen.IconFileLocked("Images\\EasyAndFastGUI\\Controls\\full_screen.bmp"); } //--- 从按钮中移除焦点 m_button_fullscreen.MouseFocus(false); m_button_fullscreen.Update(true); return(true); }
自定义 ON_CLICK_BUTTON 事件到达时在控件的事件处理程序中调用 CWindow::OnClickFullScreenButton() 方法。
//+------------------------------------------------------------------+ //| 图表事件处理程序 | //+------------------------------------------------------------------+ void CWindow::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam) { //--- 处理点击窗体按钮事件 if(id==CHARTEVENT_CUSTOM+ON_CLICK_BUTTON) { ... //--- 检查全屏模式 if(OnClickFullScreenButton((uint)lparam,(uint)dparam)) return; ... //--- return; } }
结果如下:
图例. 1. 切换到全屏并返回的示例。
为了切换到全屏模式并返回, 双击窗口标题, 现在就可以在控件的事件处理程序中处理这个双击 (ON_DOUBLE_CLICK) 窗口标题 事件。
void CWindow::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam) { //--- 处理双击对象的事件 if(id==CHARTEVENT_CUSTOM+ON_DOUBLE_CLICK) { //--- 如果事件在窗口标题中生成 if(CursorInsideCaption(m_mouse.X(),m_mouse.Y())) OnClickFullScreenButton(m_button_fullscreen.Id(),m_button_fullscreen.Index()); //--- return; } }
这就是它如何工作的:
图例. 2. 通过双击标题切换到全屏的演示。
现在我们来研究通过拖动其边框来调整窗口大小的方式。使用 CWindow::ResizeMode() 方法来启用它。
class CWindow : public CElement { private: //--- 窗口调整大小模式 bool m_xy_resize_mode; //--- public: //--- 调整窗口大小的能力 bool ResizeMode(void) const { return(m_xy_resize_mode); } void ResizeMode(const bool state) { m_xy_resize_mode=state; } }; //+------------------------------------------------------------------+ //| 构造函数 | //+------------------------------------------------------------------+ CWindow::CWindow(void) : m_xy_resize_mode(false) { ... }
若要跟踪鼠标左键点击窗口边框事件, 需要在 ENUM_MOUSE_STATE 枚举中多添加一个 (PRESSED_INSIDE_BORDER) 标识符, 它位于 Enums.mqh 文件中。
//+------------------------------------------------------------------+ //| Enums.mqh | //| 版权所有 2015, MetaQuotes 软件公司| //| https://www.mql5.com | //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| 鼠标左键按住区域枚举 | //+------------------------------------------------------------------+ enum ENUM_MOUSE_STATE { NOT_PRESSED =0, PRESSED_INSIDE =1, PRESSED_OUTSIDE =2, PRESSED_INSIDE_HEADER =3, PRESSED_INSIDE_BORDER =4 };
如果已启用调整大小模式, 则为鼠标光标创建带有新的来自 ENUM_MOUSE_POINTER 枚举 MP_WINDOW_RESIZE 标识符的图像对象。
//+------------------------------------------------------------------+ //| 指针类型枚举 | //+------------------------------------------------------------------+ enum ENUM_MOUSE_POINTER { MP_CUSTOM =0, MP_X_RESIZE =1, MP_Y_RESIZE =2, MP_XY1_RESIZE =3, MP_XY2_RESIZE =4, MP_WINDOW_RESIZE =5, MP_X_RESIZE_RELATIVE =6, MP_Y_RESIZE_RELATIVE =7, MP_X_SCROLL =8, MP_Y_SCROLL =9, MP_TEXT_SELECT =10 };
CreateResizePointer() 方法已添加到 CWindow 类中以便创建鼠标光标的图形对象:
class CWindow : public CElement { private: bool CreateResizePointer(void); }; //+------------------------------------------------------------------+ //| 创建用于调整大小的鼠标光标 | //+------------------------------------------------------------------+ bool CWindow::CreateResizePointer(void) { //--- 如果调整大小模式被禁用, 离开 if(!m_xy_resize_mode) return(true); //--- 属性 m_xy_resize.XGap(13); m_xy_resize.YGap(11); m_xy_resize.XSize(23); m_xy_resize.YSize(23); m_xy_resize.Id(CElementBase::Id()); m_xy_resize.Type(MP_WINDOW_RESIZE); //--- 创建控件 if(!m_xy_resize.CreatePointer(m_chart_id,m_subwin)) return(false); //--- return(true); }
有必要实现几种方法来调整窗口大小。我们按顺序逐一研究。
鼠标光标出现在窗口区域内时必须跟踪其位置。在此版本中, 可以通过拖动 , 右 或 底部 边框来调整窗口大小。CWindow::ResizeModeIndex() 方法跟踪所列边框之一的焦点, 并存储其它方法中后续处理的边框索引。相对于窗口的鼠标光标坐标传递给该方法进行计算。
class CWindow : public CElement { private: //--- 调整窗口大小的边框索引 int m_resize_mode_index; //--- private: //--- 返回窗口大小调整模式的索引 int ResizeModeIndex(const int x,const int y); }; //+------------------------------------------------------------------+ //| 返回窗口大小调整模式的索引 | //+------------------------------------------------------------------+ int CWindow::ResizeModeIndex(const int x,const int y) { //--- 如果已拖动, 返回边框索引 if(m_resize_mode_index!=WRONG_VALUE && m_mouse.LeftButtonState()) return(m_resize_mode_index); //--- 边框的宽度, 偏移量和索引 int width =5; int offset =15; int index =WRONG_VALUE; //--- 检查左边界的焦点 if(x>0 && x<width && y>m_caption_height+offset && y<m_y_size-offset) index=0; //--- 检查右边界的焦点 else if(x>m_x_size-width && x<m_x_size && y>m_caption_height+offset && y<m_y_size-offset) index=1; //--- 检查底部边框的焦点 else if(y>m_y_size-width && y<m_y_size && x>offset && x<m_x_size-offset) index=2; //--- 如果获得索引, 则标记点击的区域 if(index!=WRONG_VALUE) m_clamping_area_mouse=PRESSED_INSIDE_BORDER; //--- 返回区域索引 return(index); }
需要的辅助类字段: 用于确定捕获点, 存储初始尺寸和后续计算。一旦调整大小过程开始, 必须生成一条消息来形成可用控件的列表。所以, 还需要一个生成消息以便恢复控件和重置服务字段的方法: CWindow::ZeroResizeVariables()。
class CWindow : public CElement { private: //--- 与调整窗口大小相关的变量 int m_x_fixed; int m_size_fixed; int m_point_fixed; //--- private: //--- 变量清零 void ZeroResizeVariables(void); }; //+------------------------------------------------------------------+ //| 重置与调整窗口大小相关的变量 | //+------------------------------------------------------------------+ void CWindow::ZeroResizeVariables(void) { //--- 如果已经为零, 离开 if(m_point_fixed<1) return; //--- 清零 m_x_fixed =0; m_size_fixed =0; m_point_fixed =0; //--- 发送一条消息以便恢复可用的控件 ::EventChartCustom(m_chart_id,ON_SET_AVAILABLE,CElementBase::Id(),1,""); //--- 发送有关图形界面变化的消息 ::EventChartCustom(m_chart_id,ON_CHANGE_GUI,CElementBase::Id(),0,""); }
CWindow::CheckResizePointer() 方法已实现, 可判断鼠标光标显隐模式是否已为调整窗口大小准备就绪。此处, CWindow::ResizeModeIndex() 方法用于判断边框索引。
如果鼠标光标尚未显示, 则在某个边框索引处, 必须 设置相应的图标, 调整位置并输出指针。
如果边框索引的判定表明鼠标光标已经显示, 如果焦点落在其中一条边框之上, 它将会在 鼠标光标之后移动。如果没有焦点并释放鼠标左键, 光标将会消隐, 变量将被清零。
CWindow::CheckResizePointer() 方法返回 true, 如果已定义窗口边框调整的大小, 否则返回 false。
class CWindow : public CElement { private: //--- 检查调整窗口大小准备情况 bool CheckResizePointer(const int x,const int y); }; //+------------------------------------------------------------------+ //| 检查调整窗口大小准备情况 | //+------------------------------------------------------------------+ bool CWindow::CheckResizePointer(const int x,const int y) { //--- 确定当前的边框索引 m_resize_mode_index=ResizeModeIndex(x,y); //--- 如果光标被隐藏 if(!m_xy_resize.IsVisible()) { //--- 如果边框已定义 if(m_resize_mode_index!=WRONG_VALUE) { //--- 为了判断鼠标指针的显示图标索引 int index=WRONG_VALUE; //--- 如果在垂直边框 if(m_resize_mode_index==0 || m_resize_mode_index==1) index=0; //--- 如果在水平边框 else if(m_resize_mode_index==2) index=1; //--- 更改图标 m_xy_resize.ChangeImage(0,index); //--- 移动, 重绘和显示 m_xy_resize.Moving(m_mouse.X(),m_mouse.Y()); m_xy_resize.Update(true); m_xy_resize.Reset(); return(true); } } else { //--- 移动指针 if(m_resize_mode_index!=WRONG_VALUE) m_xy_resize.Moving(m_mouse.X(),m_mouse.Y()); //--- 隐藏光标 else if(!m_mouse.LeftButtonState()) { //--- 隐藏指针并重置变量 m_xy_resize.Hide(); ZeroResizeVariables(); } //--- 刷新图表 m_chart.Redraw(); return(true); } //--- return(false); }
CWindow::CheckDragWindowBorder() 方法用于检查拖动窗口边框的开始。拖动边框的那一刻, 必须将当前维度和初始拖动点的坐标存储在类的字段中。与此同时, 发送一条确定可用控件的消息。
如果后续对此方法的调用显示边框已被拖动, 则需要计算此模式中传递的距离并返回结果值。
class CWindow : public CElement { private: //--- 检查拖动窗口边框 int CheckDragWindowBorder(const int x,const int y); }; //+------------------------------------------------------------------+ //| 检查拖动窗口边框 | //+------------------------------------------------------------------+ int CWindow::CheckDragWindowBorder(const int x,const int y) { //--- 确定位移距离 int distance=0; //--- 如果边框未被拖动 if(m_point_fixed<1) { //--- 如果沿 X 轴调整大小 if(m_resize_mode_index==0 || m_resize_mode_index==1) { m_x_fixed =m_x; m_size_fixed =m_x_size; m_point_fixed =x; } //--- 如果沿 Y 轴调整大小 else if(m_resize_mode_index==2) { m_size_fixed =m_y_size; m_point_fixed =y; } //--- 发送消息来检测可用控件 ::EventChartCustom(m_chart_id,ON_SET_AVAILABLE,CElementBase::Id(),0,""); //--- 发送有关图形界面变化的消息 ::EventChartCustom(m_chart_id,ON_CHANGE_GUI,CElementBase::Id(),0,""); return(0); } //--- 如果这是左边框 if(m_resize_mode_index==0) distance=m_mouse.X()-m_x_fixed; //--- 如果这是右边框 else if(m_resize_mode_index==1) distance=x-m_point_fixed; //--- 如果这是底端边框 else if(m_resize_mode_index==2) distance=y-m_point_fixed; //---返回距离 return(distance); }
CWindow::CheckDragWindowBorder() 方法返回的结果被传递给 CWindow::CalculateAndResizeWindow() 方法, 其中窗口坐标和维度 相对于其边框 进行计算。
class CWindow : public CElement { private: //--- 计算并调整窗口大小 void CalculateAndResizeWindow(const int distance); }; //+------------------------------------------------------------------+ //| 计算并调整窗口大小 | //+------------------------------------------------------------------+ void CWindow::CalculateAndResizeWindow(const int distance) { //--- 左边框 if(m_resize_mode_index==0) { int new_x =m_x_fixed+distance-m_point_fixed; int new_x_size =m_size_fixed-distance+m_point_fixed; //--- 超过限, 离开 if(new_x<1 || new_x_size<=m_minimum_x_size) return; //--- 坐标 CElementBase::X(new_x); m_canvas.X_Distance(new_x); //--- 设置和存储大小 CElementBase::XSize(new_x_size); m_canvas.XSize(new_x_size); m_canvas.Resize(new_x_size,m_canvas.YSize()); } //--- 右边框 else if(m_resize_mode_index==1) { int gap_x2 =m_chart_width-m_mouse.X()-(m_size_fixed-m_point_fixed); int new_x_size =m_size_fixed+distance; //--- 超过限, 离开 if(gap_x2<1 || new_x_size<=m_minimum_x_size) return; //--- 设置和存储大小 CElementBase::XSize(new_x_size); m_canvas.XSize(new_x_size); m_canvas.Resize(new_x_size,m_canvas.YSize()); } //--- 低边框 else if(m_resize_mode_index==2) { int gap_y2=m_chart_height-m_mouse.Y()-(m_size_fixed-m_point_fixed); int new_y_size=m_size_fixed+distance; //--- 超过限, 离开 if(gap_y2<2 || new_y_size<=m_minimum_y_size) return; //--- 设置和存储大小 m_full_height=new_y_size; CElementBase::YSize(new_y_size); m_canvas.YSize(new_y_size); m_canvas.Resize(m_canvas.XSize(),new_y_size); } }
CWindow::CheckDragWindowBorder() 和 CWindow::CheckDragWindowBorder() 是在 CWindow::UpdateSize() 方法内部调用。在此, 在方法开始时, 检查是否按下鼠标左键。如果按钮被释放, 则与窗口调整大小相关的变量的所有值被重置, 且程序离开该方法。
如果按下鼠标左键, 则 (1) 确定拖动状态下边框经过的距离, (2) 计算并调整窗口大小, (3) 重绘窗口并 (4) 调整其元素的位置。
在方法结束时, 根据坐标轴调整窗口的大小, 生成一个事件, 稍后将用于调整附加到窗口的所有控件的大小, 并启用相应的模式。
class CWindow : public CElement { private: //--- 更新窗口大小 void UpdateSize(const int x,const int y); }; //+------------------------------------------------------------------+ //| 更新窗口大小 | //+------------------------------------------------------------------+ void CWindow::UpdateSize(const int x,const int y) { //--- 如果完成并释放鼠标左键, 重置数值 if(!m_mouse.LeftButtonState()) { ZeroResizeVariables(); return; } //--- 如果边框的捕获和移动尚未开始, 离开 int distance=0; if((distance=CheckDragWindowBorder(x,y))==0) return; //--- 计算并调整窗口大小 CalculateAndResizeWindow(distance); //--- 重绘窗口 Update(true); //--- 更新对象的位置 Moving(m_x,m_y); //--- 窗口大小已更改的消息 if(m_resize_mode_index==2) ::EventChartCustom(m_chart_id,ON_WINDOW_CHANGE_YSIZE,(long)CElementBase::Id(),0,""); else ::EventChartCustom(m_chart_id,ON_WINDOW_CHANGE_XSIZE,(long)CElementBase::Id(),0,""); }
所有列出的测量窗口尺寸的方法均在主要方法 CWindow::ResizeWindow() 中调用。它首先检查窗口是否可用。然后, 如果鼠标左键并非在窗口边框之上按下, 则程序会离开方法。然后再进行三次检查: (1) 如果启用调整大小模式, (2) 如果窗口最大化为全屏, (3) 未最小化。
如果通过所有检查, 则获取鼠标光标的相对坐标, 如果捕获到窗口边框, 则 调整控件大小。
class CWindow : public CElement { private: //--- 控制窗口大小 void ResizeWindow(void); }; //+------------------------------------------------------------------+ //| 控制窗口大小 | //+------------------------------------------------------------------+ void CWindow::ResizeWindow(void) { //--- 如果窗口不可用, 离开 if(!IsAvailable()) return; //--- 如果鼠标按钮没有在窗体按钮上按下, 离开 if(m_clamping_area_mouse!=PRESSED_INSIDE_BORDER && m_clamping_area_mouse!=NOT_PRESSED) return; //--- 如果 (1) 窗口调整大小模式被禁用或 // (2) 窗口处于全屏或 (3) 窗口最小化, 离开 if(!m_xy_resize_mode || m_is_fullscreen || m_is_minimized) return; //--- 坐标 int x =m_mouse.RelativeX(m_canvas); int y =m_mouse.RelativeY(m_canvas); //--- 检查是否改变列表的宽度 if(!CheckResizePointer(x,y)) return; //--- 更新窗口大小 UpdateSize(x,y); }
当鼠标光标移动的事件到达时 (CHARTEVENT_MOUSE_MOVE), CWindow::ResizeWindow() 方法在事件处理程序中调用。
这就是它如何工作的:
图例. 3. 通过移动其边框调整窗口大小的示例。
表格单元中的文本框和组合框
如果表格单元含有不同的控件, 表格变为非常灵活的工具用来管理其内包含的数据。最接近的示例可以在 MetaTrader 交易终端中的 MQL 应用程序的设置窗口的 "输入参数" 选项卡中, 或"策略测试器" 窗口的 "参数" 选项卡上查看。具有此类功能的图形界面将使 MQL 应用程序达到新的水平。
图例. 4. MQL 程序的设置窗口。
图例. 5. 在策略测试器中设置 MQL 应用程序。
之前的文章 之一将复选框和按钮添加到表格单元。现在, 我们研究文本编辑框和组合框的实现。
首先, 在 Enums.mqh 文件中的 ENUM_TYPE_CELL 枚举中添加了两个新的标识符, 表示表格单元类型:
- CELL_COMBOBOX – 组合框类型的单元。
- CELL_EDIT – 单元的文本编辑框类型。
//+------------------------------------------------------------------+ //| 枚举表格单元类型 | //+------------------------------------------------------------------+ enum ENUM_TYPE_CELL { CELL_SIMPLE =0, CELL_BUTTON =1, CELL_CHECKBOX =2, CELL_COMBOBOX =3, CELL_EDIT =4 };
为了实现计划, 只需在表格中创建一个文本编辑框控件 (CTextEdit) 和/或一个组合框控件 (CComboBox), 作为 CTable 控件的组件。当它的数值被修改时, 它们将出现在双击单元格上。
当指定 CELL_EDIT 或 CELL_COMBOBOX 类型的情况下, 使用 CTable::CellType() 方法设置单元格类型时, 必须在类的特殊字段中 设置一次标志。
//+------------------------------------------------------------------+ //| 用来创建渲染表格的类 | //+------------------------------------------------------------------+ class CTable : public CElement { private: //--- 存在含有文本编辑框和组合框的表格单元 bool m_edit_state; bool m_combobox_state; //--- public: //--- 设置/获取单元类型 void CellType(const uint column_index,const uint row_index,const ENUM_TYPE_CELL type); }; //+------------------------------------------------------------------+ //| 设置单元类型 | //+------------------------------------------------------------------+ void CTable::CellType(const uint column_index,const uint row_index,const ENUM_TYPE_CELL type) { //--- 检查数组超界 if(!CheckOutOfRange(column_index,row_index)) return; //--- 设置单元类型 m_columns[column_index].m_rows[row_index].m_type=type; //--- 文本编辑框存在的标志 if(type==CELL_EDIT && !m_edit_state) m_edit_state=true; //--- 组合框存在的标志 else if(type==CELL_COMBOBOX && !m_combobox_state) m_combobox_state=true; }
创建表格时, 如果没有设置 CELL_EDIT 或 CELL_COMBOBOX 的单元, 则不会创建相应类型的控件。如果需要, 可以获得指向这些控件的指针。
class CTable : public CElement { private: //--- 用于创建表格的对象 CTextEdit m_edit; CComboBox m_combobox; //--- private: bool CreateEdit(void); bool CreateCombobox(void); //--- public: //--- 返回控件指针 CTextEdit *GetTextEditPointer(void) { return(::GetPointer(m_edit)); } CComboBox *GetComboboxPointer(void) { return(::GetPointer(m_combobox)); } };
当处理表格双击时, 调用 CTable::CheckCellElement() 方法。它包含 CELL_EDIT 和 CELL_COMBOBOX 类型单元的相应补充。以下所列代码显示了此方法的缩写版本。处理不同单元类型的方法 将在下面详细描述.
//+------------------------------------------------------------------+ //| 当点击时, 检查单元格控件是否被激活 | //+------------------------------------------------------------------+ bool CTable::CheckCellElement(const int column_index,const int row_index,const bool double_click=false) { //--- 如果单元格内没有控件, 离开 if(m_columns[column_index].m_rows[row_index].m_type==CELL_SIMPLE) return(false); //--- switch(m_columns[column_index].m_rows[row_index].m_type) { ... //--- 如果这是一个含有文本编辑框的单元格 case CELL_EDIT : { if(!CheckPressedEdit(column_index,row_index,double_click)) return(false); //--- break; } //--- 如果这是一个含有组合框的单元格 case CELL_COMBOBOX : { if(!CheckPressedCombobox(column_index,row_index,double_click)) return(false); //--- break; } } //--- return(true); }
在继续研究使用控件处理表格单元点击的方法之前, 我们要留意 CTextBox 类型控件的补充。有时, 当文本框激活时, 文本框中包含的所有文本都将被自动选择, 并将文本光标移动到行尾。这便于快速输入和替换所有文本。
当前版本中的文本自动选择仅适用于单行文本框。可以使用 CTextBox::AutoSelectionMode() 方法启用此模式。
//+------------------------------------------------------------------+ //| 创建多行文本框的类 | //+------------------------------------------------------------------+ class CTextBox : public CElement { private: //--- 自动文本选择模式 bool m_auto_selection_mode; //--- public: //--- 自动文本选择模式 void AutoSelectionMode(const bool state) { m_auto_selection_mode=state; } };
已经实现了私有方法 CTextBox::SelectAllText() 来完整选择文本框中的文本。此处, 首先获得第一行的字符数, 并设置文本选择的索引。接着, 可见的文本区域必须一直移动到右侧。最后, 文本光标必须移动到行尾。
class CTextBox : public CElement { private: //--- 选择所有文本 void SelectAllText(void); }; //+------------------------------------------------------------------+ //| 选择所有文本 | //+------------------------------------------------------------------+ void CTextBox::SelectAllText(void) { //--- 获取字符数组的大小 int symbols_total=::ArraySize(m_lines[0].m_symbol); //--- 设置选择文本的索引 m_selected_line_from =0; m_selected_line_to =0; m_selected_symbol_from =0; m_selected_symbol_to =symbols_total; //--- 将水平滚动条的滑块移动到最后的位置 HorizontalScrolling(); //--- 将光标移动到行尾 SetTextCursor(symbols_total,0); }
双击表格单元后, 编辑框将出现, 但为了避免再次单击从而激活文本框, 将需要一个附加的 public CTextBox::ActivateTextBox() 方法。调用它会模拟文本框上的点击。为此, 简单地调用 CTextBox::OnClickTextBox() 方法, 将控件的图形对象名称传递它。在此方法中将选择文本。
class CTextBox : public CElement { public: //--- 激活文本框 void ActivateTextBox(void); }; //+------------------------------------------------------------------+ //| 激活文本框 | //+------------------------------------------------------------------+ void CTextBox::ActivateTextBox(void) { OnClickTextBox(m_textbox.Name()); }
由于整个表格只使用一个文本框, 所以必须调整大小, 因为单元格可能有不同的宽度。因此, 添加了一个额外的 public CTextBox::ChangeSize() 方法, 它调用以前在其它文章中研究并已实现的方法。
class CTextBox : public CElement { public: //--- 调整大小 void ChangeSize(const uint x_size,const uint y_size); }; //+------------------------------------------------------------------+ //| 调整大小 | //+------------------------------------------------------------------+ void CTextBox::ChangeSize(const uint x_size,const uint y_size) { //--- 设置新的大小 ChangeMainSize(x_size,y_size); //--- 计算文本框的大小 CalculateTextBoxSize(); //--- 为文本框设置新尺寸 ChangeTextBoxSize(); }
双击带有文本框的单元格将调用 CTable::CheckPressedEdit() 方法。在此还需要 用来保存最后所编辑单元索引的类字段, 以便处理输入完毕事件 (ON_END_EDIT)。
在当前版本中, 只能通过双击单元格来调用文本编辑框。因此, 在方法伊始有这样的检查。接下来, 存储传递的列和行索引。要将文本编辑框正确放置在表格单元上方, 在计算坐标时必须考虑表格沿两条坐标轴的偏移量。另外, 在计算中考虑了头部的存在。之后, 需要计算并设置文本框的大小, 并且 在单元格的当前位置插入要显示的字符串。然后, 激活文本框并显示, 重新绘制图表来反映最新的变化。
class CTable : public CElement { private: //--- 最后编辑单元格的列和行的索引 int m_last_edit_row_index; int m_last_edit_column_index; //--- private: //--- 检查是否在带有文本框的单元格上点击 bool CheckPressedEdit(const int column_index,const int row_index,const bool double_click=false); }; //+------------------------------------------------------------------+ //| 检查是否在单元格的文本框上单击 | //+------------------------------------------------------------------+ bool CTable::CheckPressedEdit(const int column_index,const int row_index,const bool double_click=false) { //--- 如果不是双击, 离开 if(!double_click) return(false); //--- 保存索引 m_last_edit_row_index =row_index; m_last_edit_column_index =column_index; //--- 沿两个坐标轴的偏移 int x_offset=(int)m_table.GetInteger(OBJPROP_XOFFSET); int y_offset=(int)m_table.GetInteger(OBJPROP_YOFFSET); //--- 设置新坐标 m_edit.XGap(m_columns[column_index].m_x-x_offset); m_edit.YGap(m_rows[row_index].m_y+((m_show_headers)? m_header_y_size : 0)-y_offset); //--- 大小 int x_size =m_columns[column_index].m_x2-m_columns[column_index].m_x+1; int y_size =m_cell_y_size+1; //--- 设置大小 m_edit.GetTextBoxPointer().ChangeSize(x_size,y_size); //--- 设置表格单元中的值 m_edit.SetValue(m_columns[column_index].m_rows[row_index].m_full_text); //--- 激活文本框 m_edit.GetTextBoxPointer().ActivateTextBox(); //--- 设置焦点 m_edit.GetTextBoxPointer().MouseFocus(true); //--- 显示文本框 m_edit.Reset(); //--- 重绘图表 m_chart.Redraw(); return(true); }
一旦在单元格中输入了数值, 就会生成一个带有 ON_END_EDIT 标识符的事件, 此事件必须在表格的事件处理程序中接收。已实现的 CTable::OnEndEditCell() 方法来处理此事件。如果存在含有文本框的单元格, 且标识符匹配, 则 为表格单元设置新值。之后, 文本框必须 失活并隐藏。
class CTable : public CElement { private: //--- 处理单元格中数值输入结束 bool OnEndEditCell(const int id); }; //+------------------------------------------------------------------+ //| 事件处理器 | //+------------------------------------------------------------------+ void CTable::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam) { ... //--- 处理输入结束事件 if(id==CHARTEVENT_CUSTOM+ON_END_EDIT) { if(OnEndEditCell((int)lparam)) return; //--- return; } ... } //+------------------------------------------------------------------+ //| 处理单元格中数值输入结束 | //+------------------------------------------------------------------+ bool CTable::OnEndEditCell(const int id) { //--- 如果 (1) 标识符不匹配, 或 (2) 没有含文本框的单元格, 离开 if(id!=CElementBase::Id() || !m_edit_state) return(false); //--- 为表格单元设置数值 SetValue(m_last_edit_column_index,m_last_edit_row_index,m_edit.GetValue(),0,true); Update(); //--- 失活并隐藏文本框 m_edit.GetTextBoxPointer().DeactivateTextBox(); m_edit.Hide(); m_chart.Redraw(); return(true); }
单击已激活的文本框外边应隐藏该文本框。为此, 将需要 CTable::OnEndEditCell() 方法。另外, 文本框必须 失活, 以便下次调用时正确显示。当鼠标左键状态改变事件 (ON_CHANGE_MOUSE_LEFT_BUTTON) 到达时, 会在表格的事件处理程序里调用 CTable::OnEndEditCell() 方法。如果单元格里存在组合框时, 在CTable::CheckAndHideCombobox() 方法里同理进行检查。这个方法的代码不会在这里提供, 因为它几乎与已研究过的一样。
class CTable : public CElement { private: //--- 检查单元格中的控件是否隐藏 void CheckAndHideEdit(void); }; //+------------------------------------------------------------------+ //| 事件处理器 | //+------------------------------------------------------------------+ void CTable::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam) { ... //--- 更改鼠标左键的状态 if(id==CHARTEVENT_CUSTOM+ON_CHANGE_MOUSE_LEFT_BUTTON) { ... //--- 检查单元格中的文本框是否被隐藏 CheckAndHideEdit(); //--- 检查单元格中的组合框是否隐藏 CheckAndHideCombobox(); return; } ... } //+------------------------------------------------------------------+ //| 检查单元格中的文本框是否被隐藏 | //+------------------------------------------------------------------+ void CTable::CheckAndHideEdit(void) { //--- 如果 (1) 没有文本框, 或 (2) 它被隐藏, 离开 if(!m_edit_state || !m_edit.IsVisible()) return; //--- 检查焦点 m_edit.GetTextBoxPointer().CheckMouseFocus(); //--- 如果 (1) 失焦, (2) 按下鼠标左键, 失活并隐藏文本框 if(!m_edit.GetTextBoxPointer().MouseFocus() && m_mouse.LeftButtonState()) { m_edit.GetTextBoxPointer().DeactivateTextBox(); m_edit.Hide(); m_chart.Redraw(); } }
现在我们来研究如何从表格单元中调用组合框的方法。对于 CELL_COMBOBOX 类型的单元格, 需要一个数组来存储组合框列表的值, 以及用于保存所选项索引的附加字段。数组 和 字段 已被添加到 CTCell 结构中。
class CTable : public CElement { private: //--- 表格单元的属性 struct CTCell { ... string m_value_list[]; // 数值数组 (用于含有组合框的单元格) int m_selected_item; // 组合框列表中的选定项 ... }; };
在创建表之前为自定义类中的单元格指定组合框类型 (CELL_COMBOBOX) 时, 必须将数值列表传递给组合框列表。
这是由 CTable::AddValueList() 方法完成的。此方法还传递组合框列表中的选择项索引和单元索引。省缺选择第一项 (索引 0)。
数组超界检查位于方法的开头。之后, 将 CTCell 结构中的数组设置为与传递的数组相同的大小, 并创建数值副本。在数组超界的情况下调整所选项索引, 且还保存在 CTCell 结构中。为单元格设置所选项的文本。
class CTable : public CElement { public: //--- 将数值列表添加到组合框 void AddValueList(const uint column_index,const uint row_index,const string &array[],const uint selected_item=0); }; //+------------------------------------------------------------------+ //| 将数值列表添加到组合框 | //+------------------------------------------------------------------+ void CTable::AddValueList(const uint column_index,const uint row_index,const string &array[],const uint selected_item=0) { //--- 检查数组超界 if(!CheckOutOfRange(column_index,row_index)) return; //--- 设置指定单元格的列表大小 uint total=::ArraySize(array); ::ArrayResize(m_columns[column_index].m_rows[row_index].m_value_list,total); //--- 存储传递的值 ::ArrayCopy(m_columns[column_index].m_rows[row_index].m_value_list,array); //--- 检查列表中所选项的索引 uint check_item_index=(selected_item>=total)? total-1 : selected_item; //--- 在列表中保存所选项 m_columns[column_index].m_rows[row_index].m_selected_item=(int)check_item_index; //--- 保存在单元格中选择的文本 m_columns[column_index].m_rows[row_index].m_full_text=array[check_item_index]; }
CTable::CheckPressedCombobox() 方法处理双击含有组合框的单元格。在此, 首先存储单元索引以便在选择列表项的情况下进行后续处理。然后相对于单元格的左上角设置组合框坐标。之后, 其 控件设置为单元格同样大小。为了在运行时调整按钮 (CButton) 和列表 (CListView) 的大小, ChangeSize() 方法已添加到类中, 还有两个字段。由于列表大小可能因单元格而异, 必须每次重建并重新填充列表。接着, 重新绘制组合框元素, 并令其可见。在方法的最后, 生成关于图形界面变化的事件。
class CTable : public CElement { private: //--- 检查是否在含有组合框的单元格上点击 bool CheckPressedCombobox(const int column_index,const int row_index,const bool double_click=false); }; //+------------------------------------------------------------------+ //| 检查单元格中的组合框是否被单击 | //+------------------------------------------------------------------+ bool CTable::CheckPressedCombobox(const int column_index,const int row_index,const bool double_click=false) { //--- 如果不是双击, 离开 if(!double_click) return(false); //--- 保存索引 m_last_edit_row_index =row_index; m_last_edit_column_index =column_index; //--- 沿两个坐标轴的偏移 int x_offset=(int)m_table.GetInteger(OBJPROP_XOFFSET); int y_offset=(int)m_table.GetInteger(OBJPROP_YOFFSET); //--- 设置新坐标 m_combobox.XGap(m_columns[column_index].m_x-x_offset); m_combobox.YGap(m_rows[row_index].m_y+((m_show_headers)? m_header_y_size : 0)-y_offset); //--- 设置按钮大小 int x_size =m_columns[column_index].m_x2-m_columns[column_index].m_x+1; int y_size =m_cell_y_size+1; m_combobox.GetButtonPointer().ChangeSize(x_size,y_size); //--- 设置列表大小 y_size=m_combobox.GetListViewPointer().YSize(); m_combobox.GetListViewPointer().ChangeSize(x_size,y_size); //--- 设置单元列表大小 int total=::ArraySize(m_columns[column_index].m_rows[row_index].m_value_list); m_combobox.GetListViewPointer().Rebuilding(total); //--- 设置来自单元格的列表 for(int i=0; i<total; i++) m_combobox.GetListViewPointer().SetValue(i,m_columns[column_index].m_rows[row_index].m_value_list[i]); //--- 设置来自单元格的项目 int index=m_columns[column_index].m_rows[row_index].m_selected_item; m_combobox.SelectItem(index); //--- 更新控件 m_combobox.GetButtonPointer().MouseFocus(true); m_combobox.GetButtonPointer().Update(true); m_combobox.GetListViewPointer().Update(true); //--- 显示文本框 m_combobox.Reset(); //--- 重绘图表 m_chart.Redraw(); //--- 发送有关图形界面变化的消息 ::EventChartCustom(m_chart_id,ON_CHANGE_GUI,CElementBase::Id(),0,""); return(true); }
在组合框列表 (ON_CLICK_COMBOBOX_ITEM) 中选择项目的事件由 CTable::OnClickComboboxItem() 方法处理。在此首先检查标识符是否匹配, 且表格中是否存在组合框。如果这些检查完成, 则根据先前存储的索引在单元格中设置 所选项索引 和 所选项的值。
class CTable : public CElement { private: //--- 处理单元格下拉列表中项目的选择 bool OnClickComboboxItem(const int id); }; //+------------------------------------------------------------------+ //| 事件处理器 | //+------------------------------------------------------------------+ void CTable::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam) { ... //--- 处理列表中选择项目的事件 if(id==CHARTEVENT_CUSTOM+ON_CLICK_COMBOBOX_ITEM) { if(OnClickComboboxItem((int)lparam)) return; //--- return; } ... } //+------------------------------------------------------------------+ //| 处理单元格组合框中项目的选择 | //+------------------------------------------------------------------+ bool CTable::OnClickComboboxItem(const int id) { //--- 如果 (1) 标识符不匹配, 或 (2) 没有含组合框的单元格, 离开 if(id!=CElementBase::Id() || !m_combobox_state) return(false); //--- 最后编辑的单元格索引 int c=m_last_edit_column_index; int r=m_last_edit_row_index; //--- 存储单元格中选择项的索引 m_columns[c].m_rows[r].m_selected_item=m_combobox.GetListViewPointer().SelectedItemIndex(); //--- 为表格单元设置数值 SetValue(c,r,m_combobox.GetValue(),0,true); Update(); return(true); }
最后, 一切都会如此模样:
图例. 6. 在表格单元中使用文本框和组合框的示例。
应用测试
为了测试目的, 已经创建了一个 MQL 应用程序, 其中包含表格 (CTable) 和多行文本框 (CTextBox) 控件。在表格的第一列中, 所有单元格都包含复选框控件 (CELL_CHECKBOX)。在表格的第二列中, 单元格带有 "文本框" 类型 (CELL_EDIT)。在第三列中, 单元格交替设置为 "组合框" (CELL_COMBOBOX) 和 "文本框" (CELL_EDIT) 类型。在第四列中, 单元格设置为 "按钮" 类型 (CELL_BUTTON)。MQL 应用程序的自定义类的事件处理程序将响应事件并输出到多行文本框中。
这就是它如何工作的:
图例. 7. 用于测试的 MQL 应用程序已完工。
本文附带的存档中提供了此应用程序, 以便进行更详细的研究。
结束语
表格现在可以创建 "文本框" 和 "组合框" 类型的单元格。控件的窗体现在可以通过拖动其边框来扩展到全屏或手动调整大小。
函数库当前开发阶段的总体规划|:
图例. 8. 当前开发阶段的函数库结构
所呈现的函数库代码是免费的。您可以在您的项目中使用它, 包括商用项目, 撰写文章和履行订单。
如果您在使用本文中的素材时有任何疑问, 您可在评论中咨询。